﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace memcached
{
    /// <summary>
    /// Read-ahead cache store wrapper. Will refresh registered items just before they expire,
    /// so that any Get will result in a valid item (not null).
    /// </summary>
    /// <remarks>Will measure average duration of the provider to fetch each item and use it to
    /// predict the time to make item refresh. Precision is on a level of 1 second.</remarks>
    public class ReadAheadCache : ICache, IDisposable
    {
        /// <summary>
        /// Initialize for a specific store.
        /// </summary>
        /// <param name="store">has to be thread-safe</param>
        public ReadAheadCache(ICache store)
        {
            this.store = store;
        }

        #region Public
        /// <summary>
        /// Mark specific key for rea-ahread refresh using value provider.
        /// </summary>
        /// <remarks>This will not fetch the item.</remarks>
        /// <param name="key">not null</param>
        /// <param name="valueProvider">returning not-null values for a key in a timely fashion</param>
        public void Register(string key, Func<ICache, string, object> valueProvider)
        {
            if (valueProvider == null)
                throw new ArgumentNullException("valueProvider");
            if (key == null)
                throw new ArgumentNullException("key");
            Item i = new Item();
            i.Provider = valueProvider;
            i.FetchDuration = 1;
            i.Key = key;
            lock (items)
                items[key] = i;
        }

        /// <summary>
        /// Start this instance, make it monitor all registered (now or later) items.
        /// </summary>
        public void Start()
        {
            if (timer != null)
                timer.Change(1000, 1000);
            else
                timer = new System.Threading.Timer(ProcessTimer, null, 1000, 1000);
        }

        /// <summary>
        /// Stop this instance.
        /// </summary>
        public void Dispose()
        {
            if (timer != null)
            {
                timer.Dispose();
                timer = null;
            }
            lock (items)
                items.Clear();
        }

        public int GetAvgDuration(string key)
        {
            Item item;
            lock (items)
                items.TryGetValue(key, out item);
            if (item != null && item.ExpiresAt != DateTime.MinValue)
            {
                return item.FetchDuration;
            }
            return -1;
        }
        #endregion

        #region Private
        private readonly ICache store;

        private System.Threading.Timer timer;

        private readonly Dictionary<string, Item> items = new Dictionary<string, Item>(1);

        private sealed class Item
        {
            public string Key;

            public DateTime ExpiresAt;

            public int ValidFor;

            public int FetchDuration;

            public Func<ICache, string, object> Provider;
        }

        private int ExecuteWithMeasure(Action a)
        {
            PerformanceTimer t = new PerformanceTimer();
            t.Start();
            a();
            t.Stop();
            // for this to make sense, we need to be ahead of time
            return (int)Math.Round(t.Seconds + 1, MidpointRounding.AwayFromZero);
        }

        // Invoked once a second
        private void ProcessTimer(object state)
        {
            DateTime now = DateTime.Now;
            string[] keys = items.Keys.ToArray();
            foreach (string key in keys)
            {
                Item item;
                lock (items)
                    items.TryGetValue(key, out item);
                if (item != null && item.ExpiresAt != DateTime.MinValue)
                {
                    // we need to act FetchDuration seconds earlier to the last expiry time set
                    DateTime aheadTime = item.ExpiresAt.AddSeconds(-item.FetchDuration);
                    if (now > aheadTime)
                    {
                        // invoke item fetching when reached the time
                        System.Threading.ThreadPool.QueueUserWorkItem(ProcessItemAhead, item);
                    }
                }
            }
        }

        private void TryRefresh(string key)
        {
            Item item;
            lock (items)
                items.TryGetValue(key, out item);
            if (item != null && item.ExpiresAt != DateTime.MinValue)
            {
                DateTime aheadTime = item.ExpiresAt.AddSeconds(-item.FetchDuration);
                if (DateTime.Now > aheadTime)
                {
                    ProcessItemAhead(item);
                }
            }
        }

        // Invoked to refresh the item before it expires
        private void ProcessItemAhead(object state)
        {
            Item i = (Item)state;
            object value = null;
            try
            {
                int duration = ExecuteWithMeasure(() =>
                {
                    value = i.Provider(store, i.Key);
                });
                i.FetchDuration = (i.FetchDuration + duration) / 2;
                if (value == null)
                    System.Diagnostics.Trace.TraceWarning(string.Format(
                        "ReadAheadCache: Got NULL from value provider on key {0}", i.Key));
            }
            catch (Exception e)
            {
                System.Diagnostics.Trace.TraceError(string.Format(
                        "ReadAheadCache: Value provider failed on key {0} with: {1}", i.Key, e.Message));
                return;
            }
            if (!store.Set(i.Key, i.ValidFor, value))
            {
                System.Diagnostics.Trace.TraceWarning(string.Format(
                    "ReadAheadCache: Failed to update ahead item {0}", i.Key));
            }
            else
                i.ExpiresAt = DateTime.Now.AddSeconds(i.ValidFor);
        }

        private void RemoveItem(string key)
        {
            lock (items)
                items.Remove(key);
        }

        private void RefreshItem(string key, int validFor)
        {
            Item item;
            lock (items)
                items.TryGetValue(key, out item);
            if (item != null)
            {
                item.ValidFor = validFor;
                item.ExpiresAt = DateTime.Now.AddSeconds(validFor);
            }
        }
        #endregion

        #region Interface
        /// <summary>
        /// Notice: using this will un-register the item.
        /// </summary>
        public bool Set(string key, object value)
        {
            RemoveItem(key);
            return store.Set(key, value);
        }

        public bool Set(string key, int validFor, object value)
        {
            RefreshItem(key, validFor);
            return store.Set(key, validFor, value);
        }

        public bool Set(string key, DateTime expireAt, object value)
        {
            RefreshItem(key, (int)expireAt.Subtract(DateTime.Now).TotalSeconds);
            return store.Set(key, expireAt, value);
        }

        public bool Add(string key, int validFor, object value)
        {
            RefreshItem(key, validFor);
            return store.Add(key, validFor, value);
        }

        public bool Add(string key, DateTime expireAt, object value)
        {
            RefreshItem(key, (int)expireAt.Subtract(DateTime.Now).TotalSeconds);
            return store.Add(key, expireAt, value);
        }

        /// <summary>
        /// Notice: using this will un-register the item.
        /// </summary>
        public bool Remove(string key)
        {
            RemoveItem(key);
            return store.Remove(key);
        }

        public T Get<T>(string key, out bool success)
        {
            TryRefresh(key);
            return store.Get<T>(key, out success);
        }

        public object Get(string key, out bool success)
        {
            TryRefresh(key);
            return store.Get(key, out success);
        }
        #endregion
    }
}